Многопоточное программирование в Java - Тимур Машнин
Метод tryConvertToOptimisticRead освобождает блокировку и возвращает штамп для наблюдения.
Также нужно отметить, что планировщик потоков в случае StampedLock не всегда предпочитает читателей над писателями или наоборот.
Приоритеты потоков
Для потоков мы можем устанавливать приоритеты с помощью метода setPriority класса Thread.
Установив приоритет потока выше, мы сигнализируем, что этот поток должен получить больше процессорного времени, чем потоки с более низким приоритетом.
Установка приоритета потоков не гарантирует, что поток фактически получит больше процессорного времени, так как другие факторы, такие как ожидание ресурсов, могут влиять на производительность потока с более высоким приоритетом.
Также способ, которым базовая операционная система реализует многозадачность, также может влиять на производительность потоков, и поток с более низким приоритетом может в некоторых условиях фактически получать больше времени процессора, чем поток с более высоким приоритетом.
Но в общем, потоки с более высоким приоритетом получат больше процессорного времени, чем потоки с более низким приоритетом.
Существует три константы класса Thread, определяющие приоритет потока MIN_PRIORITY, NORM_PRIORITY и MAX_PRIORITY.
Атомарные переменные
Как мы увидели, ключевой задачей многопоточного программирования является управление доступом параллельных потоков к общим ресурсам.
И мы узнали об использовании блокировок различными способами.
В многопоточном программировании есть такое понятие, как критическая секция.
При многопоточном программировании одновременный доступ к общим ресурсам может привести к неожиданному или ошибочному поведению, поэтому части программы, в которых есть доступ к общему ресурсу, защищаются. Эти защищенные части называются критическими секциями.
Предположим, у меня есть банковский счет.
И мой банковский счет, скажем, содержит 500 долларов.
И у меня есть общий банковский счет с дочерью.
Моя дочь имеет собственный банковский счет, баланс которого составляет 0 долларов.
Дочь просит у меня 100 долларов.
Я перевожу деньги со своего счета на общий счет, а дочка переводит деньги с общего счета на свой счет.
Теперь мы можем смоделировать эту операцию, используя два потока.
Есть поток T1, в котором 100 долларов вычитаются из моего баланса и добавляются к общему балансу.
И есть поток T2, где 100 долларов вычитаются из общего баланса и добавляются к балансу дочери.
Теперь вопрос в том, что может пойти не так, если это будет выполнено как многопоточная программа?
Мы видим, что здесь есть переменная общего баланса, которая читается и записывается двумя потоками.
При этом порядок чтения и записи этой общей переменной может вызвать проблемы.
Например, если чтение с одного потока вклинится между чтением и записью в другом потоке.
Поэтому эта переменная должна быть изолирована и находиться в критической секции.
Критические секции могут быть реализованы с помощью низкоуровневого или высокоуровневого интерфейса программирования.
Предположим, что у нас есть массив элементов.
И доступ к этим элементам осуществляется несколькими потоками.
И эти потоки извлекают элементы из массива и обрабатывают их.
Таким образом, каждый из этих потоков может выполнять цикл с переменной-счетчиком, которая отслеживает текущий элемент.
На каждом этапе цикла эта переменная увеличивается на единицу.
Если у вас несколько потоков, выполняющих этот код, мы уже определили проблемы, которые могут возникнуть.
В гонке потоков могут быть одновременные обращения к общей переменной и некорректная ее запись, и чтение.
Для устранения этой проблемы нужно изолировать эту переменную.
А обработка элементов может продолжаться параллельно.
Таким образом, нужно сделать переменную-счетчик атомарной.
Вместо того, чтобы реализовать это с помощью низкоуровневого программирования, и блокировать весь код, где используется эта переменная, мы можем воспользоваться пакетом java.util.concurrent.atomic, который определяет классы, поддерживающие атомарные операции для одиночных переменных.
И таким образом изолировать только эту переменную, а остальной код оставить параллельным.
При этом вместо синхронизации мы можем просто использовать объекты соответствующих классов.
Атомарные классы гарантируют выполнение определенных операций, таких как увеличение и уменьшение, обновление или добавление значения, потокобезопасным способом.
Пакет java.util.concurrent.atomic также предоставляет класс AtomicReference, который позволяет обновлять ссылку на объект атомарно.
Здесь мы на основе строки создаём объект AtomicReference, который позволяет обновить ссылку на строку атомарно.
А также этот пакет содержит классы обновления, такие как AtomicReferenceFieldUpdater, которые можно использовать для операций compareAndSet для поля volatile класса.
Метод compareAndSet означает «Обновить переменную этим новым значением, но отказать, если другой поток изменил значение после моего последнего просмотра».
В этом примере сначала создается экземпляр AtomicInteger с начальным значением 123.
Затем сравнивается значение AtomicInteger с ожидаемым значением 123, и, если они равны, новое значение AtomicInteger становится 234.
Среди других атомарных классов также есть такие классы как AtomicBoolean и AtomicLong.
Для переменных типа Float и Double атомарность можно обеспечить с помощью классов AtomicInteger и AtomicLong и методов конвертации floatToIntBits, intBitstoFloat, doubleToLongBits, и longBitsToDouble.
В случае атомарных классов недостатком является то, что, если не получается установить значение из-за гонки с другим потоком, попытка установить значение будет повторяться.
При высокой конкуренции это может превратиться в прямую блокировку, в которой поток должен постоянно пытаться установить значение в бесконечном цикле, пока он не преуспеет.
Поддержание одного единственного счетчика, суммы и т. д., значения которого обновляется, возможно, многими потоками, является общей проблемой масштабируемости.
И масштабируемая поддержка обновляемых переменных представлена с помощью таких классов, как DoubleAccumulator, DoubleAdder, LongAccumulator, LongAdder, в которых используются техники сокращения конкуренции, которые обеспечивают значительное увеличение пропускной способности по сравнению с атомарными переменными.
Классы LongAdder и DoubleAdder могут использоваться в качестве альтернативы AtomicLong для последовательного сложения чисел.
С точки зрения использования, применение этих классов очень похоже на использование атомарных классов.
Просто создается LongAdder и используются его методы, такие как intValue и add, чтобы получить и установить значение.
Магия происходит за кулисами.
Что делает этот класс, когда операция сравнения и замены не удается из-за конкуренции, этот класс хранит дельту во внутреннем объекте ячейки, выделенном для этого потока.
Затем он добавляет значение ожидающих ячеек в сумму при вызове функции intValue.
Это уменьшает необходимость возврата и выполнения операции сравнения и замены.
Таким образом, вместо того, чтобы складывать числа сразу, этот класс просто хранит у себя набор слагаемых, чтобы уменьшить